package com.gitee.easyopen.register;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
import org.springframework.util.ReflectionUtils.MethodFilter;

import com.gitee.easyopen.ApiConfig;
import com.gitee.easyopen.annotation.Api;
import com.gitee.easyopen.annotation.ApiService;
import com.gitee.easyopen.bean.ApiDefinition;
import com.gitee.easyopen.bean.DefinitionHolder;
import com.gitee.easyopen.doc.ApiDocBuilder;
import com.gitee.easyopen.doc.ApiDocHolder;
import com.gitee.easyopen.exception.DuplicateApiNameException;
import com.gitee.easyopen.message.ErrorFactory;

/**
 * api注册类,在spring启动完成时进行注册.<br>
 * 
 * <pre>
 * <code>
 * 原理:
 * 1. 在spring容器中找到被@ApiService注解的类
 * 2. 在类中找到被@Api注解的方法
 * 3. 保存方法信息以及类对象,方便后期进行invoke
 * </code>
 * </pre>
 * 
 * @author tanghc
 *
 */
public class ApiRegister {
    private static final Logger logger = LoggerFactory.getLogger(ApiRegister.class);

    private ExecutorService executorService;

    private ApiConfig apiConfig;
    private ApplicationContext applicationContext;

    public ApiRegister() {
    }

    public ApiRegister(ApiConfig apiConfig, ApplicationContext applicationContext) {
        this.apiConfig = apiConfig;
        this.applicationContext = applicationContext;
    }

    public void regist(RegistCallback registCallback) throws Exception {
        // 初始化文档
        initDocBuilder(); 
        // 初始化国际化消息
        ErrorFactory.initMessageSource(this.apiConfig.getIsvModules());

        logger.info("开始注册Api接口...");
        long startTime = System.currentTimeMillis();
        ApplicationContext ctx = this.getApplicationContext();
        Assert.notNull(ctx, "ApplicationContext不能为空");
        Assert.notNull(apiConfig, "ApiConfig不能为空");

        String[] beans = ctx.getBeanNamesForType(Object.class);

        for (String beanName : beans) {
            Class<?> beanClass = ctx.getType(beanName);
            ApiService apiServiceAnno = AnnotationUtils.findAnnotation(beanClass, ApiService.class);
            boolean hasApiServiceAnnotation = apiServiceAnno != null;

            if (hasApiServiceAnnotation) {
                Object handler = ctx.getBean(beanClass);
                // 处理beanClass类中被@Api标记的方法
                doWithMethods(beanClass, new ApiMethodProcessor(handler, apiServiceAnno),
                        new ApiMethodFilter());
            }
        }
        long endTime = System.currentTimeMillis();

        logger.info("注册Api接口完毕,耗时:" + (endTime - startTime) / 1000.0 + "秒");
        
        if(registCallback != null) {
            registCallback.onRegistFinished(apiConfig);
        }
    }
    
    // 改造ReflectionUtils.doWithMethods().
    // BUG描述：如果Service类有@Transactional注解（作用在类上或方法上），那么spring会使用cglib动态
    // 生成一个子类并且继承原有的类public ClassCGLIB extends Service {}，然后重写
    // Service的方法，实现动态代理。这时使用ReflectionUtils.doWithMethods()获取方法会拿到所有的method,
    // 其实只应该拿到cglib类中的方法即可。
    private static void doWithMethods(Class<?> clazz, MethodCallback mc, MethodFilter mf) {
        // Keep backing up the inheritance hierarchy.
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (mf != null && !mf.matches(method)) {
                continue;
            }
            try {
                mc.doWith(method);
            }
            catch (IllegalAccessException ex) {
                throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex);
            }
        }
        // ！！注意：下面这两句【必须注释掉】，如果clazz对象被CGLIB代理，那么父类就是原生类。因为CGLIB是以子类的方式生成。
        // 这里只需要获取子类的方法即可。
//        if (clazz.getSuperclass() != null) {
//            doWithMethods(clazz.getSuperclass(), mc, mf);
//        }
//        else if (clazz.isInterface()) {
//            for (Class<?> superIfc : clazz.getInterfaces()) {
//                doWithMethods(superIfc, mc, mf);
//            }
//        }
    }

    private class ApiMethodProcessor implements ReflectionUtils.MethodCallback {
        private Object handler;
        private ApiService apiServiceAnno;

        public ApiMethodProcessor(Object handler, ApiService apiServiceAnno) {
            super();
            this.handler = handler;
            this.apiServiceAnno = apiServiceAnno;
        }

        @Override
        public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
            if(!Modifier.isPublic(method.getModifiers())) {
                method.setAccessible(true);
            }
            Api api = AnnotationUtils.findAnnotation(method, Api.class);
            boolean ignoreSign = api.ignoreSign() ? true : this.apiServiceAnno.ignoreSign();
            boolean ignoreValidate = api.ignoreValidate() ? true : this.apiServiceAnno.ignoreValidate();
            
            boolean isWrapResult = this.apiServiceAnno.wrapResult() ? api.wrapResult() : false;

            ApiDefinition apiDefinition = new ApiDefinition();
            apiDefinition.setIgnoreSign(ignoreSign);
            apiDefinition.setIgnoreValidate(ignoreValidate);
            apiDefinition.setWrapResult(isWrapResult);
            apiDefinition.setHandler(handler);
            apiDefinition.setMethod(method);
            apiDefinition.setName(api.name());
            String version = api.version();
            if(version == null || "".equals(version.trim())) {
                version = apiConfig.getDefaultVersion();
            }
            apiDefinition.setVersion(version);
            Class<?>[] paramClasses = method.getParameterTypes();
            Class<?> paramClass = paramClasses.length == 0 ? null : paramClasses[0];
            apiDefinition.setMethodArguClass(paramClass);

            logger.debug("注册接口name={},version={},method={} {}({})", api.name(), api.version(),
                    method.getReturnType().getName(), method.getName(), paramClass == null ? "" : paramClass.getName());
            
            try {
                DefinitionHolder.addApiDefinition(apiDefinition);
            } catch (DuplicateApiNameException e) {
                logger.error(e.getMessage(), e);
                System.exit(0);
            }
            
            if(apiConfig.isShowDoc()) {
                // 生成doc内容
                executorService.execute(new ApiDocRunner(apiServiceAnno, api, handler, method));
            }
        }
        
    }

    // 过滤出被@Api标记的方法
    private static class ApiMethodFilter implements ReflectionUtils.MethodFilter {
        @Override
        public boolean matches(Method method) {
            return !method.isSynthetic() && AnnotationUtils.findAnnotation(method, Api.class) != null;
        }
    }
    
    private void initDocBuilder() {
        if(this.apiConfig.isShowDoc()) {
            logger.info("初始化文档生成器");
            executorService = Executors.newFixedThreadPool(1);
            ApiDocHolder.setApiDocBuilder(new ApiDocBuilder(this.apiConfig));
        }
    }

    private class ApiDocRunner implements Runnable {

        private ApiService apiService;
        private Api api;
        private Object handler;
        private Method method;
        
        public ApiDocRunner(ApiService apiService, Api api, Object handler, Method method) {
            super();
            this.apiService = apiService;
            this.api = api;
            this.handler = handler;
            this.method = method;
        }

        @Override
        public void run() {
            ApiDocHolder.getApiDocBuilder().addDocItem(apiService, api, handler, method);
        }

    }

    public ApiConfig getApiConfig() {
        return apiConfig;
    }

    public void setApiConfig(ApiConfig apiConfig) {
        this.apiConfig = apiConfig;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

}
